package dnl.toolbox.reflect;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * General reflection utility methods that are concerned with
 * <code>Methods</code>s.
 * 
 * @author Daniel
 */
public class Methods {

	public enum AccessorType {GETTER, SETTER}
	
	/**
	 * Gets all the setter methods available in class c (including methods
	 * defined in super types).
	 * 
	 * @param c
	 *            a <code>Class</code>.
	 * @return the setter methods defined in class <code>c</code>.
	 */
	public static List<Method> getSetters(Class<?> c, boolean declared) {
		return getMethodsWithPrefix(c, "set", declared, false);
	}

	/**
	 * Gets all the getter methods available in class c (including methods
	 * defined in super types).
	 * 
	 * @param c
	 *            a <code>Class</code>.
	 * @return the getter methods defined in class <code>c</code>.
	 */
	public static List<Method> getGetters(Class<?> c, boolean declared) {
		List<Method> methods = getMethodsWithPrefix(c, "get", declared, false);
		methods.addAll(getMethodsWithPrefix(c, "is", declared, false));
		return methods;
	}

	/**
	 * Gets all the getter methods available in class c (including methods
	 * defined in super types) that have a corresponding setter.
	 * 
	 * @param c
	 *            a <code>Class</code>.
	 * @return the getter methods defined in class <code>c</code>.
	 */
	public static List<Method> getAttributesGetters(Class<?> c, boolean declared) {
		List<Method> getters = getMethodsWithPrefix(c, "get", declared, true);
		getters.addAll(getMethodsWithPrefix(c, "is", declared, true));
		
		// keep aside all attribute names that match the setters
		List<Method> setters = getMethodsWithPrefix(c, "set", declared, false);
		Set<String> attributeNames = new HashSet<String>();
		for (Method setter : setters) {
			attributeNames.add(setter.getName().substring(3));
		}
		
		// if there was'nt a setter, remove that getter
		for (Iterator<Method> iterator = getters.iterator(); iterator.hasNext();) {
			Method getter = iterator.next();
			String mName = getter.getName();
			if(mName.startsWith("is")){
				mName = mName.substring(2);
			}
			else {
				mName = mName.substring(3);
			}
			if(!attributeNames.contains(mName)){
				iterator.remove();
			}
			// only a single appearance of the getter is allowed
			attributeNames.remove(mName);
		}
		
		return getters;
	}
	
	/**
	 * Gets a getter method for the given property on the given class. If the
	 * property is of a boolean type, the method will return accessors that has
	 * a 'has' or 'is' prefix.
	 * 
	 * @param c
	 *            a <code>Class</code>.
	 * @param propertyName
	 *            name of a property in the given class.
	 * @return the corresponding getter method or <code>null</code> if a
	 *         getter does not exist or in any case of access problem.
	 */
	public static Method getGetter(Class<?> c, String propertyName, boolean allowSuper){
		Method getterMethod = getPrefixedMethod(c, propertyName, AccessorType.GETTER, allowSuper);
		return getterMethod;
	}

	/**
	 * Gets a getter method for the given property on the given class. If the
	 * property is of a boolean type, the method will return accessors that has
	 * a 'has' or 'is' prefix.
	 * 
	 * @param c
	 *            a <code>Class</code>.
	 * @param propertyName
	 *            name of a property in the given class.
	 * @return the corresponding getter method or <code>null</code> if a
	 *         getter does not exist or in any case of access problem.
	 */
	public static Method getSetter(Class<?> c, String propertyName, boolean allowSuper){
		Method setterMethod = getPrefixedMethod(c, propertyName, AccessorType.SETTER, allowSuper);
		return setterMethod;
	}
	
	public static Class<?> getPropertyType(Class<?> c, String propertyName, boolean allowSuper){
		StringBuffer sb = new StringBuffer(propertyName);
		sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
		
		try {
			Method getter = allowSuper ? 
					c.getMethod("get" + sb.toString(), new Class<?>[0]) : 
					c.getDeclaredMethod("get" + sb.toString(), new Class<?>[0]);
			return getter.getReturnType();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException ignored) {
			// ok no getter
		}

		try {
			Method getter = allowSuper ? 
					c.getMethod("is" + sb.toString(), new Class<?>[0]) : 
					c.getDeclaredMethod("is" + sb.toString(), new Class<?>[0]);
			return getter.getReturnType();
		} catch (SecurityException e) {
			e.printStackTrace();
		} catch (NoSuchMethodException ignored) {
			// ok no getter
		}
		return null;
	}
	
	
	/**
	 * 
	 * @param c
	 * @param propertyName
	 * @param accessorType
	 * @return
	 */
	private static Method getPrefixedMethod(Class<?> c, String propertyName, AccessorType accessorType, boolean allowSuper){
		StringBuffer sb = new StringBuffer(propertyName);
		sb.setCharAt(0, Character.toUpperCase(sb.charAt(0)));
		
		Class<?> type = getPropertyType(c, propertyName, allowSuper);
		if(type == null){
			return null;
		}

		String[] prefixes = null;
		Class<?>[] paramTypes = null;
		
		switch (accessorType) {
		case GETTER:
			if(Boolean.class.equals(type)){
				prefixes = new String[]{"is", "has"};
			}
			else {
				prefixes = new String[]{"get"};
			}
			break;
		case SETTER:
			paramTypes = new Class[]{type};
			prefixes = new String[]{"set"};
			break;

		default:
			return null;
		}
		
		
		for (String prefix : prefixes) {
			StringBuffer sb2 = new StringBuffer(sb.toString());
			sb2.insert(0, prefix);
			Method accessorMethod = null;
			 
			try {
				accessorMethod = c.getMethod(sb2.toString(),paramTypes);
				return accessorMethod;
			} catch (Exception ignored) {
				ignored.printStackTrace();
			}
		}
		
		
		return null;		
	}
	
	public static List<Method> getMethodsWithPrefix(Class<?> c,  String prefix, boolean declared, boolean noArgs) {
		return getMethodsWithPrefix(c, null, prefix, declared, noArgs);
	}
	
	/**
	 * Gets all the setter method available in class c (including methods
	 * defined in super types).
	 * 
	 * @param c
	 *            a <code>Class</code>.
	 * @return the methods defined in class <code>c</code> which names start
	 *         with <code>prefix</code>.
	 */
	private static List<Method> getMethodsWithPrefix(Class<?> c, Class<?> returnType,  String prefix, boolean declared, boolean noArgs) {
		
		Method[] allMethods = declared ? c.getDeclaredMethods(): c.getMethods();
		List<Method> filteredMethods = new ArrayList<Method>();

		for (int i = 0; i < allMethods.length; i++) {
			if (allMethods[i].getName().startsWith(prefix)) {
				if(noArgs){
					Class<?>[] parameterTypes = allMethods[i].getParameterTypes();
					if(parameterTypes.length != 0 ){
						continue;
					}
				}
				if(returnType != null ){
					if(allMethods[i].getReturnType().equals(returnType)){
						filteredMethods.add(allMethods[i]);
					}
				}
				else {
					filteredMethods.add(allMethods[i]);
				}
			}
		}

		return filteredMethods;
	}
	
	/**
	 * Gets the property name that matches the given accessor. For example, the
	 * property name that matches a method named 'getX' is 'X'.
	 * 
	 * @param accessor
	 * @return
	 */
	public static String getPropertyName(Method accessor){
		if(! 
			(accessor.getName().startsWith("get") || 
			 accessor.getName().startsWith("is") ||
			 accessor.getName().startsWith("set"))){
			throw new IllegalArgumentException("Not an accessor!");
		}
		int offset = 3;
		if(accessor.getName().startsWith("is")){
			offset = 2;
		}
		
		StringBuffer sb = new StringBuffer(accessor.getName().substring(offset));
		sb.setCharAt(0, Character.toLowerCase(sb.charAt(0)));
		return sb.toString();
	}

	public static Object evaluateByPath(Object obj, String methodPath)
			throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
		int index = methodPath.indexOf('.');
		Class<?> c = obj.getClass();

		if (index < 0) {
			Method m = c.getMethod(methodPath, (Class[])null);
			return m.invoke(obj, (Object[])null);
		} else {
			String sub1 = methodPath.substring(0, index);
			Method m = c.getMethod(sub1, (Class[])null);
			Object o = m.invoke(obj, (Object[])null);
			String sub2 = methodPath.substring(index + 1);
			return evaluateByPath(o, sub2);
		}
	}

}